﻿ 
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;




namespace Hotel.UI.Views
{/// <summary>
 /// Precedence for Math Opertator
 /// </summary>
    public enum MathOperatorPrecedence
    {
        /// <summary>Lowest</summary>
        Lowest,
        /// <summary>Low</summary>
        Low,
        /// <summary>Medium</summary>
        Medium,
        /// <summary>High</summary>
        High,
        /// <summary>Constant</summary>
        Constant,
    }

    /// <summary>
    /// Math Operator
    /// </summary>
    public sealed class MathOperator
    {
        /// <summary>
        /// Initialize <see cref="MathOperator"/>
        /// </summary>
        /// <param name="name">Name</param>
        /// <param name="numericCount">Number of Numerals</param>
        /// <param name="precedence">Math Operator Preference</param>
        /// <param name="calculateFunc">Calculation Function</param>
        public MathOperator(
            string name,
            int numericCount,
            MathOperatorPrecedence precedence,
            Func<double[], double> calculateFunc)
        {
            Name = name;
            CalculateFunc = calculateFunc;
            Precedence = precedence;
            NumericCount = numericCount;
        }

        /// <summary>
        /// Name
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// Number of Numerals
        /// </summary>
        public int NumericCount { get; }

        /// <summary>
        /// Math Operator Precedence
        /// </summary>
        public MathOperatorPrecedence Precedence { get; }

        /// <summary>
        /// Calculation Function
        /// </summary>
        public Func<double[], double> CalculateFunc { get; }
    }
    partial class MathExpression
    {
        const NumberStyles numberStyle = NumberStyles.Float | NumberStyles.AllowThousands;

        static readonly IFormatProvider formatProvider = new CultureInfo("en-US");

        readonly FrozenSet<MathOperator> operators;
        readonly FrozenSet<double> arguments;

        public MathExpression(string expression, IEnumerable<double>? arguments = null)
        {
            if (string.IsNullOrEmpty(expression)) throw new Exception("Expression can't be null or empty.");

            var argumentList = arguments?.ToList() ?? new List<double>();

            Expression = expression.ToLower();

            var operators = new List<MathOperator>
        {
            new MathOperator("+", 2, MathOperatorPrecedence.Low, x => x[0] + x[1]),
            new MathOperator("-", 2, MathOperatorPrecedence.Low, x => x[0] - x[1]),
            new MathOperator("*", 2, MathOperatorPrecedence.Medium, x => x[0] * x[1]),
            new MathOperator("/", 2, MathOperatorPrecedence.Medium, x => x[0] / x[1]),
            new MathOperator("%", 2, MathOperatorPrecedence.Medium, x => x[0] % x[1]),
            new MathOperator("abs", 1, MathOperatorPrecedence.Medium, x => Math.Abs(x[0])),
            new MathOperator("acos", 1, MathOperatorPrecedence.Medium, x => Math.Acos(x[0])),
            new MathOperator("asin", 1, MathOperatorPrecedence.Medium, x => Math.Asin(x[0])),
            new MathOperator("atan", 1, MathOperatorPrecedence.Medium, x => Math.Atan(x[0])),
            new MathOperator("atan2", 2, MathOperatorPrecedence.Medium, x => Math.Atan2(x[0], x[1])),
            new MathOperator("ceiling", 1, MathOperatorPrecedence.Medium, x => Math.Ceiling(x[0])),
            new MathOperator("cos", 1, MathOperatorPrecedence.Medium, x => Math.Cos(x[0])),
            new MathOperator("cosh", 1, MathOperatorPrecedence.Medium, x => Math.Cosh(x[0])),
            new MathOperator("exp", 1, MathOperatorPrecedence.Medium, x => Math.Exp(x[0])),
            new MathOperator("floor", 1, MathOperatorPrecedence.Medium, x => Math.Floor(x[0])),
            new MathOperator("ieeeremainder", 2, MathOperatorPrecedence.Medium, x => Math.IEEERemainder(x[0], x[1])),
            new MathOperator("log", 2, MathOperatorPrecedence.Medium, x => Math.Log(x[0], x[1])),
            new MathOperator("log10", 1, MathOperatorPrecedence.Medium, x => Math.Log10(x[0])),
            new MathOperator("max", 2, MathOperatorPrecedence.Medium, x => Math.Max(x[0], x[1])),
            new MathOperator("min", 2, MathOperatorPrecedence.Medium, x => Math.Min(x[0], x[1])),
            new MathOperator("pow", 2, MathOperatorPrecedence.Medium, x => Math.Pow(x[0], x[1])),
            new MathOperator("round", 2, MathOperatorPrecedence.Medium, x => Math.Round(x[0], Convert.ToInt32(x[1]))),
            new MathOperator("sign", 1, MathOperatorPrecedence.Medium, x => Math.Sign(x[0])),
            new MathOperator("sin", 1, MathOperatorPrecedence.Medium, x => Math.Sin(x[0])),
            new MathOperator("sinh", 1, MathOperatorPrecedence.Medium, x => Math.Sinh(x[0])),
            new MathOperator("sqrt", 1, MathOperatorPrecedence.Medium, x => Math.Sqrt(x[0])),
            new MathOperator("tan", 1, MathOperatorPrecedence.Medium, x => Math.Tan(x[0])),
            new MathOperator("tanh", 1, MathOperatorPrecedence.Medium, x => Math.Tanh(x[0])),
            new MathOperator("truncate", 1, MathOperatorPrecedence.Medium, x => Math.Truncate(x[0])),
            new MathOperator("^", 2, MathOperatorPrecedence.High, x => Math.Pow(x[0], x[1])),
            new MathOperator("pi", 0, MathOperatorPrecedence.Constant, _ => Math.PI),
            new MathOperator("e", 0, MathOperatorPrecedence.Constant, _ => Math.E),
        };

            if (argumentList.Count > 0)
            {
                operators.Add(new MathOperator("x", 0, MathOperatorPrecedence.Constant, _ => argumentList[0]));
            }

            for (var i = 0; i < argumentList.Count; i++)
            {
                var index = i;
                operators.Add(new MathOperator($"x{i}", 0, MathOperatorPrecedence.Constant, _ => argumentList[index]));
            }

            this.operators = operators.ToFrozenSet();
            this.arguments = argumentList.ToFrozenSet();
        }

        internal string Expression { get; }

        public double Calculate()
        {
            var rpn = GetReversePolishNotation(Expression);

            var stack = new Stack<double>();

            foreach (var value in rpn)
            {
                if (double.TryParse(value, numberStyle, formatProvider, out var numeric))
                {
                    stack.Push(numeric);
                    continue;
                }

                var mathOperator = operators.FirstOrDefault(x => x.Name == value) ??
                    throw new ArgumentException($"Invalid math expression. Can't find operator or value with name \"{value}\".");

                if (mathOperator.Precedence is MathOperatorPrecedence.Constant)
                {
                    stack.Push(mathOperator.CalculateFunc(Array.Empty<double>()));
                    continue;
                }

                var operatorNumericCount = mathOperator.NumericCount;

                if (stack.Count < operatorNumericCount)
                {
                    throw new ArgumentException("Invalid math expression.");
                }

                var args = new List<double>();
                for (var j = 0; j < operatorNumericCount; j++)
                {
                    args.Add(stack.Pop());
                }

                args.Reverse();

                stack.Push(mathOperator.CalculateFunc(args.ToArray()));
            }

            if (stack.Count != 1)
            {
                throw new ArgumentException("Invalid math expression.");
            }

            return stack.Pop();
        }

        //[GeneratedRegex(@"(?<!\d)\-?(?:\d+\.\d+|\d+)|\+|\-|\/|\*|\(|\)|\^|\%|\,|\w+")]
        //private static partial Regex MathExpressionRegexPattern();

        IEnumerable<string> GetReversePolishNotation(string expression)
        {
            var matches = Regex.Matches(expression, @"(?<!\d)\-?(?:\d+\.\d+|\d+)|\+|\-|\/|\*|\(|\)|\^|\%|\,|\w+")// MathExpressionRegexPattern().Matches(expression)
                                                          ?? throw new ArgumentException("Invalid math expression.");

            var output = new List<string>();
            var stack = new Stack<(string Name, MathOperatorPrecedence Precedence)>();

            foreach (var match in matches.Cast<Match>())
            {
                if (string.IsNullOrEmpty(match?.Value))
                {
                    continue;
                }

                var value = match.Value;

                if (double.TryParse(value, numberStyle, formatProvider, out var numeric))
                {
                    if (numeric < 0)
                    {
                        var isNegative = output.Count == 0 || stack.Count != 0;

                        if (!isNegative)
                        {
                            stack.Push(("-", MathOperatorPrecedence.Low));
                            output.Add(Math.Abs(numeric).ToString());
                            continue;
                        }
                    }

                    output.Add(value);
                    continue;
                }

                var @operator = operators.FirstOrDefault(x => x.Name == value);
                if (@operator != null)
                {
                    if (@operator.Precedence is MathOperatorPrecedence.Constant)
                    {
                        output.Add(value);
                        continue;
                    }

                    while (stack.Count > 0)
                    {
                        var (name, precedence) = stack.Peek();
                        if (precedence >= @operator.Precedence)
                        {
                            output.Add(stack.Pop().Name);
                        }
                        else
                        {
                            break;
                        }
                    }

                    stack.Push((value, @operator.Precedence));
                }
                else if (value is "(")
                {
                    stack.Push((value, MathOperatorPrecedence.Lowest));
                }
                else if (value is ")")
                {
                    var isFound = false;
                    for (var i = stack.Count - 1; i >= 0; i--)
                    {
                        if (stack.Count == 0)
                        {
                            throw new ArgumentException("Invalid math expression.");
                        }

                        var stackValue = stack.Pop().Name;
                        if (stackValue is "(")
                        {
                            isFound = true;
                            break;
                        }

                        output.Add(stackValue);
                    }

                    if (!isFound)
                    {
                        throw new ArgumentException("Invalid math expression.");
                    }
                }
                else if (value is ",")
                {
                    while (stack.Count > 0)
                    {
                        var (name, precedence) = stack.Peek();
                        if (precedence >= MathOperatorPrecedence.Low)
                        {
                            output.Add(stack.Pop().Name);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }

            for (var i = stack.Count - 1; i >= 0; i--)
            {
                var (name, precedence) = stack.Pop();
                if (name is "(")
                {
                    throw new ArgumentException("Invalid math expression.");
                }

                output.Add(name);
            }

            return output;
        }
    }
}
